/*
 * DISPLAY.C
 *
 * Colorful, sorted and optionally rich directory enumeration
 * for Windows.
 *
 * This module implements low level routines to display formatted text to the
 * console or to files, in unicode or ansi as desired, pausing if too much
 * output has occurred, etc.
 *
 * Copyright (c) 2014-15 Malcolm J. Smith
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "sdir.h"
#include "vt.h"

//
//  Display support
//

BOOL
SdirWriteRawStringToOutputDevice(
    __in HANDLE hConsole,
    __in LPCTSTR OutputString,
    __in DWORD Length
    )
{
    DWORD Written;
    if (Opts->OutputWithConsoleApi) {
        return WriteConsole(hConsole, OutputString, Length, &Written, NULL);
#ifdef UNICODE
    } else if (!Opts->OutputIsUnicode) {
        CHAR AsciiStackBuffer[128 + 1];
        LPSTR StringBuffer;
    
        if (Length < sizeof(AsciiStackBuffer)) {
    
            WideCharToMultiByte(CP_OEMCP,
                                0,
                                OutputString,
                                Length,
                                AsciiStackBuffer,
                                sizeof(AsciiStackBuffer),
                                NULL,
                                NULL);
        
            return WriteFile(hConsole, AsciiStackBuffer, Length, &Written, NULL);
    
        } else {
            BOOL ret;
    
            StringBuffer = HeapAlloc(GetProcessHeap(), 0, Length + 1);
    
            //
            //  We'd normally display an error here, but we're probably here
            //  because we're trying to display an error - that's where large
            //  strings can arrive here.
            //
    
            if (StringBuffer == NULL) {
                return FALSE;
            }
    
            WideCharToMultiByte(CP_OEMCP,
                                0,
                                OutputString,
                                Length,
                                StringBuffer,
                                Length + 1,
                                NULL,
                                NULL);
        
            ret = WriteFile(hConsole, StringBuffer, Length, &Written, NULL);
    
            HeapFree(GetProcessHeap(), 0, StringBuffer);
    
            return ret;
        }
#endif
    } else {
        return WriteFile(hConsole, OutputString, Length * sizeof(TCHAR), &Written, NULL);
    }

    //
    //  Unreachable but keeps the compiler happy
    //

    return TRUE;
}

WORD SdirCurrentAttribute = 0;

BOOL
SdirSetConsoleTextAttribute(
    __in HANDLE hConsole,
    __in WORD Attribute
    )
{
    if (Attribute == SdirCurrentAttribute) {
        return TRUE;
    }

    SdirCurrentAttribute = Attribute;

    //
    //  If we're using native APIs for formatting, do that now
    //  and return.  Note that we might be outputting to a file
    //  in which case the native API is pointless, and this is
    //  a no-op.
    //

#if SDIR_SUPPORT_VT
    if (Opts->OutputVt100Color) {
        return VtSetConsoleTextAttribute(hConsole, Attribute);
    }
#endif

    if (Opts->OutputWithConsoleApi) {
        return SetConsoleTextAttribute(hConsole, Attribute);
    }
    return TRUE;
}

BOOL
SdirWrite (
    __in_ecount(count) PSDIR_FMTCHAR str,
    __in DWORD count
    )
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD i, j;

    WORD CacheAttr;
    TCHAR CharCache[64];

    //
    //  We firstly load as much as we can into a stack buffer, then write it
    //  out.  Those syscalls are expensive - run in a slow VM to see.  We
    //  have to write out the buffer either when the next character has 
    //  different color (so we can't use the same WriteConsole call) or when
    //  the stack buffer is full.
    //

    j = 0;
    for (i = 0; i < count; i++) { 

        CacheAttr = str[i].Attr;
        CharCache[j] = str[i].Char;
        j++;

        if (( i + 1 < count && str[i + 1].Attr != CacheAttr) || (j >= sizeof(CharCache)/sizeof(CharCache[0]))) {

            SdirSetConsoleTextAttribute(hConsole, CacheAttr);
            SdirWriteRawStringToOutputDevice(hConsole, CharCache, j);
            j = 0;
        }
    }

    //
    //  If we have anything left, flush it now.
    //

    if (j > 0) {

        SdirSetConsoleTextAttribute(hConsole, CacheAttr);
        SdirWriteRawStringToOutputDevice(hConsole, CharCache, j);
    }

    return TRUE;
}

BOOL
SdirGetConsoleScreenBufferInfo(
    __in HANDLE hConsole,
    __out PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo
    )
{
    if (Opts->OutputWithConsoleApi) {
        return GetConsoleScreenBufferInfo(hConsole, lpConsoleScreenBufferInfo);
    } else {
        SetLastError(ERROR_INVALID_HANDLE);
        return FALSE;
    }
}

BOOL
SdirQueryVtSupport()
{
#ifdef SDIR_SUPPORT_VT
    DWORD SupportedFlags;

    SupportedFlags = VtQuerySupport();

    if (SupportedFlags & VT_SUPPORT_COLOR) {
        Opts->OutputVt100Color = TRUE;
    }

    if (SupportedFlags & VT_SUPPORT_EXTDCHARS) {
        Opts->OutputExtendedCharacters = TRUE;
    }

#endif

    return TRUE;
}

DWORD SdirWriteStringLinesDisplayed = 0;

BOOL
SdirWriteStringWithAttribute (
    __in LPCTSTR str,
    __in WORD DefaultAttribute
    )
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD LinesInBuffer = 0;
    DWORD TCharsInBuffer = 0;
    DWORD OffsetOfLastLineBreak;
    BOOLEAN HitLineLimit = FALSE;
    CONSOLE_SCREEN_BUFFER_INFO ScreenInfo;

    SdirSetConsoleTextAttribute(hConsole, DefaultAttribute);

    if (Opts->OutputHasAutoLineWrap) {

        GetConsoleScreenBufferInfo(hConsole, &ScreenInfo);

        while (str[TCharsInBuffer] != '\0') {
            OffsetOfLastLineBreak = 0;
            while (str[TCharsInBuffer] != '\0') {
                if (str[TCharsInBuffer] == '\n') {
                    LinesInBuffer++;
                    OffsetOfLastLineBreak = TCharsInBuffer;
                } else if (TCharsInBuffer + ScreenInfo.dwCursorPosition.X - OffsetOfLastLineBreak >= Opts->ConsoleBufferWidth) {
                    LinesInBuffer++;
                    ScreenInfo.dwCursorPosition.X = 0;
                    OffsetOfLastLineBreak = TCharsInBuffer;
                }
                TCharsInBuffer++;
                if (SdirWriteStringLinesDisplayed + LinesInBuffer >= Opts->ConsoleHeight - 1) {
                    HitLineLimit = TRUE;
                    break;
                }
            }

            SdirWriteStringLinesDisplayed += LinesInBuffer;

            SdirWriteRawStringToOutputDevice(hConsole, str, TCharsInBuffer);

            LinesInBuffer = 0;
            str += TCharsInBuffer;
            TCharsInBuffer = 0;

            //
            //  This is screwy because we call it and it calls back into us.
            //  It works because we've just reset lines displayed to zero
            //  and we "know" we have enough space to display it.  Just in
            //  case it uses different attributes, reset those now.
            //

            if (HitLineLimit) {
                SdirWriteStringLinesDisplayed = 0;
                if (Opts->EnablePause && !SdirPressAnyKey()) {
                    return FALSE;
                }
                SdirSetConsoleTextAttribute(hConsole, DefaultAttribute);
                HitLineLimit = FALSE;
            }
        }

    } else {

        while (*str != '\0') {

            //
            //  Look for line break or end of string
            //

            for (TCharsInBuffer = 0; str[TCharsInBuffer] != '\0' && str[TCharsInBuffer] != '\n'; TCharsInBuffer++);

            //
            //  If we have data, ignoring the line break or end of string, write it
            //

            if (TCharsInBuffer > 0) {

                SdirSetConsoleTextAttribute(hConsole, DefaultAttribute);
                SdirWriteRawStringToOutputDevice(hConsole, str, TCharsInBuffer);
            }

            //
            //  If we have a line break, use whatever desired break characters instead
            //

            if (str[TCharsInBuffer] == '\n') {

                SdirWriteRawStringToOutputDevice(hConsole, Opts->NewLine, (DWORD)_tcslen(Opts->NewLine));
                TCharsInBuffer++;
            }

            //
            //  And advance to the next portion of string
            //

            str += TCharsInBuffer;
        }
    }

    return TRUE;
}

BOOL
SdirRowDisplayed()
{
    if (Opts->EnablePause) {
        SdirWriteStringLinesDisplayed++;

        if (SdirWriteStringLinesDisplayed >= Opts->ConsoleHeight - 1) {

            SdirWriteStringLinesDisplayed = 0;
            if (!SdirPressAnyKey()) {
                return FALSE;
            }
            SdirWriteStringLinesDisplayed = 0;
        }
    }
    return TRUE;
}

BOOL
SdirPasteStrAndPad (
    __out_ecount(padsize) PSDIR_FMTCHAR str,
    __in_ecount(count) LPTSTR src,
    __in WORD attr,
    __in DWORD count,
    __in DWORD padsize
    )
{
    ULONG i;

    for (i = 0; i < count && i < padsize; i++) {
        str[i].Char = src[i];
        str[i].Attr = attr;
    }

    for (; i < padsize; i++) {
        str[i].Char = ' ';
        str[i].Attr = attr;
    }

    return TRUE;
}


BOOL
SdirDisplayError (
    __in LONG ErrorCode,
    __in LPCTSTR Prefix
    )
{
    LPTSTR OutputBuffer;

    if (FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
            NULL,
            ErrorCode,
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&OutputBuffer,
            0,
            NULL)) {

        if (Prefix) {
            SdirWriteStringWithAttribute(Prefix, Opts->FtError.HighlightColor);
            SdirWriteStringWithAttribute(_T(": "), Opts->FtError.HighlightColor);
        }
        if (!SdirWriteStringWithAttribute(OutputBuffer, Opts->FtError.HighlightColor)) {
            LocalFree((PVOID)OutputBuffer);
            return FALSE;
        }
        LocalFree((PVOID)OutputBuffer);
    }
    return TRUE;
}

BOOL
SdirPressAnyKey ()
{
    HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE);
    INPUT_RECORD InputBuffer;
    DWORD NumRead;

    SdirWriteString(_T("Press any key to continue..."));

    //
    //  Loop throwing away events until we get a key pressed
    //

    while (ReadConsoleInput(hConsole, &InputBuffer, 1, &NumRead) &&
           NumRead > 0 &&
           (InputBuffer.EventType != KEY_EVENT ||
            !InputBuffer.Event.KeyEvent.bKeyDown ||
            InputBuffer.Event.KeyEvent.uChar.UnicodeChar == '\0'));

    if (InputBuffer.Event.KeyEvent.uChar.UnicodeChar == 'q' ||
        InputBuffer.Event.KeyEvent.uChar.UnicodeChar == 'Q') {

        return FALSE;
    }

    //
    //  Keep throwing away any remaining events so we're not left
    //  with stale events next time we run
    //

    while (PeekConsoleInput(hConsole, &InputBuffer, 1, &NumRead) &&
           NumRead > 0) {

        ReadConsoleInput(hConsole, &InputBuffer, 1, &NumRead);
    }

    SdirWriteString(_T("\n"));
    SdirWriteStringLinesDisplayed = 0;
    return TRUE;
}

// vim:sw=4:ts=4:et:
